﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.IO;

using Pg.BioMedics.SDR.Entities;
using Pg.BioMedics.SDR.Entities.Codes;
using Pg.BioMedics.SDR.Data;
using Pg.BioMedics.SDR.Data.Caching;
using Pg.BioMedics.SDR.ServiceAgents;

namespace Pg.BioMedics.SDR.Services.Impl
{
    /// <summary>
    /// Indexing service implementation. This class provides logic for documents indexing
    /// </summary>
    internal class IndexingService : IIndexingService
    {
        #region IIndexingService Members

        /// <summary>
        /// Contains logic for a new document registration and 
        /// indexing flow initialization.
        /// </summary>
        /// <param name="document">A new document</param>
        /// <param name="content">Document content</param>
        /// <param name="author">Document's author</param>
        /// <returns>Newly registered document</returns>
        public Document RegisterDocument(Document document, Content content, Person author)
        {
            // input params validation
            if (document == null)
                throw new ServiceException("Could not register a new document, document can not be null");
            else if (content == null)
                throw new ServiceException("Could not register a new document, content cannot be null");
            else if (author == null)
                throw new ServiceException("Could not register a new document, author can not be null");
            else if (author.Id == null)
                throw new ServiceException("Could not register a new document, author must be saved first");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext())
                {
                    // insert content
                    if (String.IsNullOrEmpty(content.FileName))
                        content.FileName = String.Format("{0}_{1}", document.Title, DateTime.Now.ToString());
                    else if (content.FileName.Contains(Path.DirectorySeparatorChar))
                        content.FileName = Path.GetFileName(content.FileName);

                    content = dataContext.ContentManager.InsertContent(content);
                    dataContext.ContentManager.SetContentStatus(content, ContentStatusCodes.Saved);

                    // insert document
                    document.ContentId = content.Id.Value;
                    document = dataContext.DocumentManager.InsertDocument(document);
                    dataContext.DocumentManager.SetDocumentStatus(document, DocumentStatusCodes.Created, author);                    

                    // if document is an audio document create an annotation from the document                    
                    if (document.TypeCode == DocumentTypeCodes.Audio)
                    {
                        if (content.TypeCode == ContentTypeCodes.WAV)
                        {
                            // TODO : helper extracting other audio formats to wav
                            Annotation annotation = new Annotation();
                            annotation.DocumentId = document.Id.Value;
                            annotation.ContentId = content.Id.Value;
                            annotation.LanguageCode = (String.IsNullOrEmpty(document.LanguageCode) ? CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName
                                                                                                   : document.LanguageCode);

                            // business rule for sub-word and word annotations must be defined. Till this we use only word 
                            // based annotations
                            annotation.TypeCode = AnnotationTypeCodes.Word;

                            annotation = dataContext.AnnotationManager.InsertAnnotation(annotation);
                            dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Created, author);

                            // send annotation for extraction to ASR server
                            using (ServiceAgentsContext serviceAgentsContext = new ServiceAgentsContext())
                            {
                                Guid taskUniqueId = serviceAgentsContext.ASRAgent.EnqueueTask(content.FileName, content.Data);
                                annotation.AsrTaskUniqueId = taskUniqueId;

                                dataContext.AnnotationManager.UpdateAnnotation(annotation);
                                dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Extracting, author);
                            }
                        }
                    }
                    else if (document.TypeCode == DocumentTypeCodes.Text) // textual documents indexing
                    {
                        if (content.TypeCode == ContentTypeCodes.TEXT)
                        {
                            Annotation annotation = new Annotation();
                            annotation.DocumentId = document.Id.Value;
                            annotation.ContentId = content.Id.Value;
                            annotation.LanguageCode = document.LanguageCode;

                            // business rule for sub-word and word annotations must be defined. Till this we use only word 
                            // based annotations
                            annotation.TypeCode = AnnotationTypeCodes.Word;

                            annotation = dataContext.AnnotationManager.InsertAnnotation(annotation);
                            dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Created, author);

                            // extract tokens
                            string text = TextHelper.RemoveNonAlphaNumeric(Encoding.Default.GetString(content.Data));
                            IList<Token> tokens = TokenHelper.ExtractTokens(text);

                            // preprocess tokens
                            tokens = TokenHelper.RemoveStopTokens(tokens);
                            tokens = TokenHelper.ExpandWithSynonyms(tokens);
                            tokens = TokenHelper.RemoveStopTokens(tokens);

                            foreach (Token token in tokens)
                            {
                                token.AnnotationId = annotation.Id.Value;
                                dataContext.AnnotationManager.InsertToken(token);
                            }

                            dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Extracted, author);
                        }
                    }

                    // commit transaction and return saved document
                    dataContext.Complete();

                    document.StatusCode = DocumentStatusCodes.Created;
                    document.StatusDate = DateTime.Now;
                    document.StatusAuthorLogin = author.Login;
                    return document;
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Could not register a new document", ex);
            }
        }

        /// <summary>
        /// Conteins logic for a new document registration and indexing flow initialization.
        /// This method contains logic for appropriate entities instantiation.
        /// </summary>
        /// <param name="title">Document title</param>
        /// <param name="description">Document description</param>
        /// <param name="fileName">File name</param>
        /// <param name="fileLocation">Content file temporary location</param>
        /// <param name="authorLogin">Author login</param>
        /// <returns>Registered document</returns>
        public Document RegisterDocument(string title, string description, string fileName, string fileLocation, string authorLogin)
        {
            // input params validation
            if (String.IsNullOrEmpty(title))
                throw new ServiceException("Couldnot register document, title cannot be null or empty");
            else if (String.IsNullOrEmpty(description))
                throw new ServiceException("Couldnot register document, description cannot be null or empty");
            else if (String.IsNullOrEmpty(fileName))
                throw new ServiceException("Couldnot register document, description cannot be null or empty");
            else if (!File.Exists(fileLocation))
                throw new ServiceException("Couldnot register document, temporary file doesnot exist");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext())
                {
                    // get author
                    Person author = dataContext.PersonManager.GetPersonByLogin(authorLogin);
                    if (author == null)
                        throw new ServiceException("Person {0} doesnot exist", authorLogin);

                    // prepare and insert content
                    Content content = ContentHelper.PrepareContent(fileName, fileLocation);    
                    content = dataContext.ContentManager.InsertContent(content);
                    dataContext.ContentManager.SetContentStatus(content, ContentStatusCodes.Saved);

                    // prepare and insert document
                    Document document = new Document();
                    document.Title = title;
                    document.Description = description;
                    document.LanguageCode = CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName;
                    document.TypeCode = DocumentHelper.ResolveDocumentType(content);
                    document.ContentId = content.Id.Value;
                    document = dataContext.DocumentManager.InsertDocument(document);
                    dataContext.DocumentManager.SetDocumentStatus(document, DocumentStatusCodes.Created, author);                    

                    // if document is an audio document create an annotation from the document                    
                    if (document.TypeCode == DocumentTypeCodes.Audio)
                    {
                        if (content.TypeCode == ContentTypeCodes.WAV)
                        {
                            // TODO : helper extracting other audio formats to wav
                            Annotation annotation = new Annotation();
                            annotation.DocumentId = document.Id.Value;
                            annotation.ContentId = content.Id.Value;
                            annotation.LanguageCode = document.LanguageCode;

                            // business rule for sub-word and word annotations must be defined. Till this we use only word 
                            // based annotations
                            annotation.TypeCode = AnnotationTypeCodes.Word;

                            annotation = dataContext.AnnotationManager.InsertAnnotation(annotation);
                            dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Created, author);

                            // send annotation for extraction to ASR server
                            using (ServiceAgentsContext serviceAgentsContext = new ServiceAgentsContext())
                            {
                                Guid taskUniqueId = serviceAgentsContext.ASRAgent.EnqueueTask(content.FileName, content.Data);
                                annotation.AsrTaskUniqueId = taskUniqueId;

                                dataContext.AnnotationManager.UpdateAnnotation(annotation);
                                dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Extracting, author);
                            }
                        }
                    }
                    else if (document.TypeCode == DocumentTypeCodes.Text) // textual documents indexing
                    {
                        if (content.TypeCode == ContentTypeCodes.TEXT)
                        {
                            Annotation annotation = new Annotation();
                            annotation.DocumentId = document.Id.Value;
                            annotation.ContentId = content.Id.Value;
                            annotation.LanguageCode = document.LanguageCode;

                            // business rule for sub-word and word annotations must be defined. Till this we use only word 
                            // based annotations
                            annotation.TypeCode = AnnotationTypeCodes.Word;

                            annotation = dataContext.AnnotationManager.InsertAnnotation(annotation);
                            dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Created, author);

                            // extract tokens
                            string text = TextHelper.RemoveNonAlphaNumeric(Encoding.Default.GetString(content.Data));
                            IList<Token> tokens = TokenHelper.ExtractTokens(text);

                            // preprocess tokens
                            tokens = TokenHelper.RemoveStopTokens(tokens);
                            tokens = TokenHelper.ExpandWithSynonyms(tokens);
                            tokens = TokenHelper.RemoveStopTokens(tokens);

                            foreach (Token token in tokens)
                            {
                                token.AnnotationId = annotation.Id.Value;
                                dataContext.AnnotationManager.InsertToken(token);
                            }

                            dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Extracted, author);
                        }
                    }

                    // commit transaction and return saved document
                    dataContext.Complete();

                    document.StatusCode = DocumentStatusCodes.Created;
                    document.StatusDate = DateTime.Now;
                    document.StatusAuthorLogin = author.Login;
                    return document;
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Could not register a new document", ex);
            }

        }

        /// <summary>
        /// Contains logic for extracting list of documents authored by the given person
        /// </summary>
        /// <param name="author">Author person</param>
        /// <returns>List of documents</returns>
        public IList<Document> GetDocuments(Person author)
        {
            // input params validation
            if (author == null)
                throw new ServiceException("Couldnot get list of documents for the given author, author could not be null");
            else if (author.Id == null)
                throw new ServiceException("Couldnot get list of documents for the given author, author must be saved first");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    return dataContext.DocumentManager.GetDocumentsByStatus(DocumentStatusCodes.Created, author);
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot get documents for the given author", ex);
            }
        }

        /// <summary>
        /// Contains logic for extracting list of documents authored by the given person
        /// </summary>
        /// <param name="authorLogin">Author login</param>
        /// <returns>List of documents</returns>
        public IList<Document> GetDocuments(string authorLogin)
        {
            // input params validation
            if (String.IsNullOrEmpty(authorLogin))
                throw new ServiceException("Couldnot get list of documents for the given author, author login could not be null or empty string");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    Person author = dataContext.PersonManager.GetPersonByLogin(authorLogin);
                    return dataContext.DocumentManager.GetDocumentsByStatus(DocumentStatusCodes.Created, author);
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot get documents for the given author", ex);
            }
        }

        /// <summary>
        /// Contains logic for extracting list of pending documents authored by the given person
        /// </summary>
        /// <param name="author">Author person</param>
        /// <returns>List of documents</returns>
        public IList<Document> GetPendingDocuments(Person author)
        {
            // input params validation
            if (author == null)
                throw new ServiceException("Couldnot get list of documents for the given author, author could not be null");
            else if (author.Id == null)
                throw new ServiceException("Couldnot get list of documents for the given author, author must be saved first");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    return (from document in dataContext.DocumentManager.GetDocumentsByStatus(DocumentStatusCodes.Created, author)
                            let annotations = dataContext.AnnotationManager.GetAnnotationsByDocument(document)
                            where annotations.Count == 0
                            select document).ToList();
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot get documents for the given author", ex);
            }
        }

        /// <summary>
        /// Contains logic for extracting list of pending documents authored by the given person
        /// </summary>
        /// <param name="authorLogin">Author login</param>
        /// <returns>List of documents</returns>
        public IList<Document> GetPendingDocuments(string authorLogin)
        {
            // input params validation
            if (String.IsNullOrEmpty(authorLogin))
                throw new ServiceException("Couldnot get list of documents for the given author, author login could not be null or empty string");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    Person author = dataContext.PersonManager.GetPersonByLogin(authorLogin);
                    return (from document in dataContext.DocumentManager.GetDocumentsByStatus(DocumentStatusCodes.Created, author)
                            let annotations = dataContext.AnnotationManager.GetAnnotationsByDocument(document)
                            where annotations.Count == 0
                            select document).ToList();
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot get documents for the given author", ex);
            }
        }

        /// <summary>
        /// Provides logic for marking document as deleted
        /// </summary>
        /// <param name="document">Existing document</param>
        /// <param name="author">Person removing document</param>
        public void RemoveDocument(Document document, Person author)
        {
            // input params validation
            if (document == null)
                throw new ServiceException("Couldnot remove document, document can not be null");
            else if (document.Id == null)
                throw new ServiceException("Couldnot remove document, document must be saved first");
            else if (author == null)
                throw new ServiceException("Couldnot remove document, author cannot be null ");
            else if (author.Id == null)
                throw new ServiceException("Couldnot remove document, author must be saved first");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext())
                {
                    // set status
                    dataContext.DocumentManager.SetDocumentStatus(document, DocumentStatusCodes.Deleted, author);

                    // commit transactions
                    dataContext.Complete();
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Could not remove document {0} by person {1}", document.Id, author.Id);
            }
        }

        /// <summary>
        /// Provides logic for a new annotation registration for 
        /// given document.
        /// </summary>
        /// <param name="document">Existing document</param>
        /// <param name="media">Audio binary media content (must be wav)</param>
        /// <param name="languageCode">Annotation language code (optional)</param>
        /// <param name="author">Annotation author</param>
        /// <returns>Registered annotation</returns>
        public Annotation RegisterAnnotation(Document document, Content content, Person author)
        {
            // input params validation
            if (document == null)
                throw new ServiceException("Couldnot register annotation, document cannot be null");
            else if (document.Id == null)
                throw new ServiceException("Couldnot register annotation, document must be saved first");
            else if (content == null)
                throw new ServiceException("Couldnot register annotation, media content cannot be null");
            else if (content.TypeCode != ContentTypeCodes.WAV)
                throw new ServiceException("Could not register annotation for document {0}, content type code {1} must be wav", document.Id, content.TypeCode);
            else if (author == null)
                throw new ServiceException("Couldnot register annotation, annotation author cannot be null");
            else if (author.Id == null)
                throw new ServiceException("Couldnot register annotation, annotation author must be saved first");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext())
                {
                    //insert annotation content object 
                    if (String.IsNullOrEmpty(content.FileName))
                        content.FileName = String.Format("{0}_{1}", document.Title, DateTime.Now.ToString());

                    content = dataContext.ContentManager.InsertContent(content);
                    dataContext.ContentManager.SetContentStatus(content, ContentStatusCodes.Saved);

                    // create and insert annotation object
                    Annotation annotation = new Annotation();
                    annotation.DocumentId = document.Id.Value;
                    annotation.ContentId = content.Id.Value;
                    annotation.LanguageCode = (String.IsNullOrEmpty(document.LanguageCode) ? CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName
                                                                                           : document.LanguageCode);

                    // business rule for sub-word and word annotations must be defined. Till this we use only word 
                    // based annotations
                    annotation.TypeCode = AnnotationTypeCodes.Word;

                    annotation = dataContext.AnnotationManager.InsertAnnotation(annotation);
                    dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Created, author);

                    // send annotation for extraction to ASR server
                    using (ServiceAgentsContext serviceAgentsContext = new ServiceAgentsContext())
                    {
                        Guid taskUniqueId = serviceAgentsContext.ASRAgent.EnqueueTask(content.FileName, content.Data);
                        annotation.AsrTaskUniqueId = taskUniqueId;

                        dataContext.AnnotationManager.UpdateAnnotation(annotation);
                        dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Extracting, author);
                    }

                    // commit and return results
                    dataContext.Complete();

                    annotation.StatusCode = AnnotationStatusCodes.Extracting;
                    annotation.StatusDate = DateTime.Now;
                    return annotation;
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot register annotation", ex);
            }
        }

        /// <summary>
        /// Provides logic for a new annotation registration. This new annotation is being created by 
        /// encapsulating provided data into appropariate entities.
        /// </summary>
        /// <param name="document">The Document neing annotated</param>
        /// <param name="fileName">Annotation file name</param>
        /// <param name="fileLocation">Annotation file location</param>
        /// <param name="authorLogin">Annotation author login</param>
        /// <returns>Registered annotation</returns>
        public Annotation RegisterAnnotation(Document document, string fileName, string fileLocation, string authorLogin)
        {
            // input params validation
            if (document == null)
                throw new ServiceException("Couldnot register annotation, annotated document cannot be null");
            else if (document.Id == null)
                throw new ServiceException("Couldnot register annotation, annotated document must be saved first");
            else if (String.IsNullOrEmpty(fileName))
                throw new ServiceException("Couldnot register annotation for document {0}, annotation file name cannot be null or empty string", document.Id);
            else if (!File.Exists(fileLocation))
                throw new ServiceException("Couldnot register annotation for document {0}, temporary file doesnot exist", document.Id);

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext())
                {
                    // get autor person
                    Person author = dataContext.PersonManager.GetPersonByLogin(authorLogin);
                    if (author == null)
                        throw new ServiceException("Author with login {0} doesnot exist", authorLogin);

                    // prepare and insert content
                    Content content = ContentHelper.PrepareContent(fileName, fileLocation);
                    content = dataContext.ContentManager.InsertContent(content);
                    dataContext.ContentManager.SetContentStatus(content, ContentStatusCodes.Saved);

                    // create and insert annotation object
                    Annotation annotation = new Annotation();
                    annotation.DocumentId = document.Id.Value;
                    annotation.ContentId = content.Id.Value;
                    annotation.LanguageCode = (String.IsNullOrEmpty(document.LanguageCode) ? CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName
                                                                                           : document.LanguageCode);

                    // business rule for sub-word and word annotations must be defined. Till this we use only word 
                    // based annotations
                    annotation.TypeCode = AnnotationTypeCodes.Word;

                    annotation = dataContext.AnnotationManager.InsertAnnotation(annotation);
                    dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Created, author);

                    // send annotation for extraction to ASR server
                    using (ServiceAgentsContext serviceAgentsContext = new ServiceAgentsContext())
                    {
                        Guid taskUniqueId = serviceAgentsContext.ASRAgent.EnqueueTask(content.FileName, content.Data);
                        annotation.AsrTaskUniqueId = taskUniqueId;

                        dataContext.AnnotationManager.UpdateAnnotation(annotation);
                        dataContext.AnnotationManager.SetAnnotationStatus(annotation, AnnotationStatusCodes.Extracting, author);
                    }

                    // commit and return results
                    dataContext.Complete();

                    annotation.StatusCode = AnnotationStatusCodes.Extracting;
                    annotation.StatusDate = DateTime.Now;
                    return annotation;
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot register annotation for document {0}", ex, document.Id);
            }
        }

        /// <summary>
        /// Provides logic for listing annotations assigned to the given document
        /// </summary>
        /// <param name="document">Exisitng document</param>
        /// <returns>List of annotations</returns>
        public IList<Annotation> GetAnnotations(Document document)
        {
            // input params validation
            if (document == null)
                throw new ServiceException("Couldnot get annotations for document, document cannot be null");
            else if (document.Id == null)
                throw new ServiceException("Couldnot get annotations for document, document must be saved first");

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    return dataContext.AnnotationManager.GetAnnotationsByDocument(document);
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot get annotations for document {0}", ex, document.Id);
            }
        }

        /// <summary>
        /// Contains logic for retriving content of the given document
        /// </summary>
        /// <param name="document">Document instance</param>
        /// <returns>Content or null if document doesnot exist</returns>
        public Content GetContent(Document document)
        {
            // input params validation
            if (document == null)
                throw new ServiceException("Couldnot get content, document cannot be null");
            else if (document.Id == null)
                throw new ServiceException("Couldnot get content, document must be saved first");
            else if (document.ContentId == null)
                throw new ServiceException("Couldnot get content for the document {0}, content id must be specified", document.Id);

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    return dataContext.ContentManager.GetContentById(document.ContentId.Value);
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Could not get content for the document {0}", ex, document.Id);
            }
        }

        #endregion
    }
}
